Skip to content

Introduce ReceiveAuthKey #3917

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open

Conversation

shaavan
Copy link
Member

@shaavan shaavan commented Jul 8, 2025

Builds on #3845

This PR builds on the work in #3845 by introducing ReceiveAuthKey, a dedicated struct that replaces the previously hardcoded [u8; 32] used for authenticating MessageContexts in incoming BlindedMessagePaths.

It:

  • Encapsulates the authentication key in a type-safe ReceiveAuthKey struct
  • Adds this key to the NodeSigner interface
  • Updates the BlindedMessagePath constructor to accept it as a parameter

This completes the original intent of #3845 — making the authentication mechanism explicit and less byte-heavy, while preserving the same security properties.

Next steps:

@ldk-reviews-bot
Copy link

ldk-reviews-bot commented Jul 8, 2025

I've assigned @valentinewallace as a reviewer!
I'll wait for their review and will help manage the review process.
Once they submit their review, I'll check if a second reviewer would be helpful.

@shaavan
Copy link
Member Author

shaavan commented Jul 8, 2025

cc @TheBlueMatt

@shaavan
Copy link
Member Author

shaavan commented Jul 9, 2025

Updated from pr3917.01 to pr3917.02 (diff):

Changes:

  1. Removed now redundant (hmac, nonce), from MessageContext

@shaavan
Copy link
Member Author

shaavan commented Jul 9, 2025

Updated from pr3917.02 to pr3917.03 (diff):

Changes:

  1. Rebase on main to resolve merge conflicts.

Copy link

codecov bot commented Jul 9, 2025

Codecov Report

Attention: Patch coverage is 96.69749% with 25 lines in your changes missing coverage. Please review.

Project coverage is 88.86%. Comparing base (79fc513) to head (f78f425).

Files with missing lines Patch % Lines
lightning-dns-resolver/src/lib.rs 44.44% 10 Missing ⚠️
lightning/src/crypto/streams.rs 90.32% 2 Missing and 4 partials ⚠️
lightning/src/onion_message/messenger.rs 92.59% 4 Missing ⚠️
lightning/src/ln/blinded_payment_tests.rs 88.88% 2 Missing ⚠️
lightning/src/ln/channelmanager.rs 75.00% 1 Missing and 1 partial ⚠️
lightning/src/crypto/poly1305.rs 99.66% 0 Missing and 1 partial ⚠️
Additional details and impacted files
@@            Coverage Diff             @@
##             main    #3917      +/-   ##
==========================================
+ Coverage   88.85%   88.86%   +0.01%     
==========================================
  Files         166      166              
  Lines      119438   119602     +164     
  Branches   119438   119602     +164     
==========================================
+ Hits       106129   106290     +161     
  Misses      10985    10985              
- Partials     2324     2327       +3     

☔ View full report in Codecov by Sentry.
📢 Have feedback on the report? Share it here.

🚀 New features to boost your workflow:
  • ❄️ Test Analytics: Detect flaky tests, report on failures, and find test suite problems.

Copy link
Collaborator

@TheBlueMatt TheBlueMatt left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Basically LGTM, a few minor nits.

Also, the last commit is beautiful:
4 files changed, 34 insertions(+), 331 deletions(-)

Comment on lines 42 to 56
const OFFER_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[5; 16];
// HMAC input for a `PaymentId`. The HMAC is used in `AsyncPaymentsContext::OutboundPayment`.
#[cfg(async_payments)]
const ASYNC_PAYMENT_ID_HMAC_INPUT: &[u8; 16] = &[6; 16];

// HMAC input for a `PaymentHash`. The HMAC is used in `OffersContext::InboundPayment`.
const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[7; 16];

// HMAC input for `ReceiveTlvs`. The HMAC is used in `blinded_path::payment::PaymentContext`.
const PAYMENT_TLVS_HMAC_INPUT: &[u8; 16] = &[8; 16];

// HMAC input used in `AsyncPaymentsContext::InboundPayment` to authenticate inbound
// held_htlc_available onion messages.
#[cfg(async_payments)]
const ASYNC_PAYMENTS_HELD_HTLC_HMAC_INPUT: &[u8; 16] = &[9; 16];
Copy link
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

We should leave comments noting the HMAC_INPUTs that were previously used for other purposes so we never reuse them again.

Copy link
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

I have added comments for documenting the older usage of the fields in pr3917.04
Let me know if it looks good!
Thanks for the pointer!

@ldk-reviews-bot
Copy link

🔔 1st Reminder

Hey @valentinewallace! This PR has been waiting for your review.
Please take a look when you have a chance. If you're unable to review, please let us know so we can find another reviewer.

@TheBlueMatt
Copy link
Collaborator

Oops, needs rebase.

@shaavan
Copy link
Member Author

shaavan commented Jul 10, 2025

Updated from pr3917.03 to pr3917.04 (diff):
Addressed @TheBlueMatt comments

Changes:

  1. Code, and commit history clean-ups
  2. Introduce cleaner documentation for the removed fields and constant, for better future reference.

}
}
pub use fuzzy_poly1305::*;
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The duplicate pub use fuzzy_poly1305::*; statement at line 426 should be removed, as it's already exported at line 397. This duplication would cause compilation errors.

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

@shaavan
Copy link
Member Author

shaavan commented Jul 10, 2025

Also, the last commit is beautiful:
4 files changed, 34 insertions(+), 331 deletions(-)

Haha, just a direct consequence of standing on the shoulders of giants. Thanks so much for the amazing ReceiveAuthKey-based authentication system! ✨

const PAYMENT_HASH_HMAC_INPUT: &[u8; 16] = &[7; 16];
// NOTE:
// The following `HMAC_INPUT` constants were previously used for authenticating fields
// in `OffersContext` and `AsyncPaymentsContext`, but were removed in LDK v0.1.X with
Copy link
Member Author

@shaavan shaavan Jul 10, 2025

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

@TheBlueMatt

which version will this be merged?

TheBlueMatt and others added 8 commits July 11, 2025 00:50
`Poly1305::raw_result` copies the output into a slice, for some
reason allowing any length sice. This isn't a great API, so while
we're here we change it to return the 16-byte tag instead.
Rather than skipping compilation of `poly1305.rs` when building for
fuzzing and relying on `ChaCha20Poly1305` to do the fuzzing
variants, implement an actual fuzz wrapper in `poly1305.rs`,
keeping the same fuzz MAC structure that we already have.

We also add a fuzzing implementation of `fixed_time_eq` which does
a simple comparison, to allow the fuzzer to "see into" the
comparison in some cases.

Best reviewed with `-b`.
`ChaChaPolyReadAdapter` decodes an arbitrary object and checks the
poly1305 tag. In the coming commits, we'll need a variant of this
which allows for an *optional* AAD in the poly1305 tag, accepting
either tag as valid, but indicating to the caller whether the AAD
was used.

We could use the actual AAD setup in poly1305, which puts the AAD
first in the MAC (and then pads it out to a multiple of 16 bytes),
but since we're gonna check both with and without, its nice to
instead put the AAD at the end, enabling us to only calculate most
of the hash once before cloning its state and adding the AAD block.

We do this by swapping the AAD and the data being MAC'd in the
AAD-containing MAC check (but leaving them where they belong for
the non-AAD-containing MAC check).

We also add a corresponding `chachapoly_encrypt_with_swapped_aad`
which allows encrypting with the new MAC format.
In the upcoming commit, we introduce the usage of a new `ReceiveAuthKey`
that will be used to authenticate message contexts in the received
`BlindedMessagePath`s.
When we receive an onion message, we often want to make sure it was
sent through a blinded path we constructed. This protects us from
various deanonymization attacks where someone can send a message to
every node on the network until they find us, effectively
unwrapping the blinded path and identifying its recipient.

We generally do so by adding authentication tags to our
`MessageContext` variants. Because the contexts themselves are
encrypted (and MAC'd) to us, we only have to ensure that they
cannot be forged, which is trivially accomplished with a simple
nonce and a MAC covering it.

This logic has ended up being repeated in nearly all of our onion
message handlers, and has gotten quite repetitive.

Instead, here, we simply authenticate the blinded path contexts
using the MAC that's already there, but tweaking it with an
additional secret as the AAD in Poly1305. This prevents forgery as
the secret is now required to make the MAC check pass.

Ultimately this means that no one can ever build a blinded path
which terminates at an LDK node that we'll accept, but over time
we've come to recognize this as a useful property, rather than
something to fight. Here we finally break from the spec fully in
our context encryption (not just the contents thereof).

This will save a bit of space in some of our `MessageContext`s,
though sadly not in the blinded path we include in `Bolt12Offer`s,
so they're generally not in space-sensitive blinded paths.

We can apply the same logic in our blinded payment paths as well,
but we do not do so here.

This commit only adds the required changes to the cryptography, for
now it uses a constant key of `[41; 32]`.
This commit replaces the hardcoded key used for authenticating the
context in incoming `BlindedMessagePath`s with a dedicated
`ReceiveAuthKey`.

This makes the authentication mechanism explicit and configurable
for the user.

Changes include:
- Introducing `ReceiveAuthKey` to the `NodeSigner`, used to authenticate
  the context at the final hop of an incoming blinded path.
- Updating `BlindedMessagePath::new` to accept a `ReceiveAuthKey` as a
  parameter during path construction.
Now that we have introduced an alternate mechanism for authentication
in the codebase, we can safely remove the now redundant (hmac, nonce)
fields from the MessageContext's while maintaining the security of the
onion messages.
@shaavan
Copy link
Member Author

shaavan commented Jul 10, 2025

Updated from pr3917.04 to pr3917.05 (diff):

Changes:

  1. Rebase on main to resolve merge conflicts.

Comment on lines +10 to +11
#[cfg(not(fuzzing))]
mod fuzzy_poly1305 {
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The module structure appears to be inverted. The #[cfg(not(fuzzing))] attribute is currently applied to the fuzzy_poly1305 module which contains the real implementation, while it should be applied to the real implementation instead. The configuration should be #[cfg(not(fuzzing))] for the real Poly1305 implementation and #[cfg(fuzzing)] for the fuzzy version. This inversion could lead to unexpected behavior when compiling with different feature flags.

Suggested change
#[cfg(not(fuzzing))]
mod fuzzy_poly1305 {
#[cfg(fuzzing)]
mod fuzzy_poly1305 {

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

Comment on lines +152 to 155
let mut iter = unblinded_path.peekable();
while let Some(pk) = iter.next() {
build_keys_in_loop!(pk, false, None);
}
Copy link

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

The iterator usage in this function has a logic error. The code calls iter.next() but doesn't properly destructure the tuple it returns. Since the iterator is yielding tuples of (pk, hop_recv_key), the loop should be:

let mut iter = unblinded_path.peekable();
while let Some((pk, hop_recv_key)) = iter.next() {
    build_keys_in_loop!(pk, false, None);
    // Use hop_recv_key as needed
}

This ensures both the public key and the optional receive key are properly extracted from each iteration.

Spotted by Diamond

Is this helpful? React 👍 or 👎 to let us know.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
None yet
Projects
None yet
Development

Successfully merging this pull request may close these issues.

3 participants